﻿/**
 * @license Copyright (c) 2003-2014, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md or http://ckeditor.com/license
 */

(function () {
    CKEDITOR.on('dialogDefinition', function (ev) {
        var tab,
            name = ev.data.name,
            definition = ev.data.definition;

        if (name == 'link') {
            definition.removeContents('target');
            definition.removeContents('upload');
            definition.removeContents('advanced');
            tab = definition.getContents('info');
            tab.remove('emailSubject');
            tab.remove('emailBody');
        } else if (name == 'image') {
            definition.removeContents('advanced');
            tab = definition.getContents('Link');
            tab.remove('cmbTarget');
            tab = definition.getContents('info');
            tab.remove('txtAlt');
            tab.remove('basic');
        }
    });

    var bbcodeMap = {
            b: 'strong',
            u: 'u',
            i: 'em',
            color: 'span',
            size: 'span',
            quote: 'blockquote',
            code: 'code',
            url: 'a',
            email: 'span',
            img: 'span',
            '*': 'li',
            list: 'ol'
        },
        convertMap = {strong: 'b', b: 'b', u: 'u', em: 'i', i: 'i', code: 'code', li: '*'},
        tagnameMap = {
            strong: 'b',
            em: 'i',
            u: 'u',
            li: '*',
            ul: 'list',
            ol: 'list',
            code: 'code',
            a: 'link',
            img: 'img',
            blockquote: 'quote'
        },
        stylesMap = {color: 'color', size: 'font-size'},
        attributesMap = {url: 'href', email: 'mailhref', quote: 'cite', list: 'listType'};

    // List of block-like tags.
    var dtd = CKEDITOR.dtd,
        blockLikeTags = CKEDITOR.tools.extend({table: 1}, dtd.$block, dtd.$listItem, dtd.$tableContent, dtd.$list);

    var semicolonFixRegex = /\s*(?:;\s*|$)/;

    function serializeStyleText(stylesObject) {
        var styleText = '';
        for (var style in stylesObject) {
            var styleVal = stylesObject[style],
                text = (style + ':' + styleVal).replace(semicolonFixRegex, ';');

            styleText += text;
        }
        return styleText;
    }

    // Maintain the map of smiley-to-description.
    var smileyMap = {
            smiley: ':)',
            sad: ':(',
            wink: ';)',
            laugh: ':D',
            cheeky: ':P',
            blush: ':*)',
            surprise: ':-o',
            indecision: ':|',
            angry: '>:(',
            angel: 'o:)',
            cool: '8-)',
            devil: '>:-)',
            crying: ';(',
            kiss: ':-*'
        },
        smileyReverseMap = {},
        smileyRegExp = [];

    // Build regexp for the list of smiley text.
    for (var i in smileyMap) {
        smileyReverseMap[smileyMap[i]] = i;
        smileyRegExp.push(smileyMap[i].replace(/\(|\)|\:|\/|\*|\-|\|/g, function (match) {
            return '\\' + match;
        }));
    }

    smileyRegExp = new RegExp(smileyRegExp.join('|'), 'g');

    var decodeHtml = (function () {
        var regex = [],
            entities = {
                nbsp: '\u00A0', // IE | FF
                shy: '\u00AD', // IE
                gt: '\u003E', // IE | FF |   --   | Opera
                lt: '\u003C' // IE | FF | Safari | Opera
            };

        for (var entity in entities)
            regex.push(entity);

        regex = new RegExp('&(' + regex.join('|') + ');', 'g');

        return function (html) {
            return html.replace(regex, function (match, entity) {
                return entities[entity];
            });
        };
    })();

    CKEDITOR.BBCodeParser = function () {
        this._ = {
            bbcPartsRegex: /(?:\[([^\/\]=]*?)(?:=([^\]]*?))?\])|(?:\[\/([a-z]{1,16})\])/ig
        };
    };

    CKEDITOR.BBCodeParser.prototype = {
        parse: function (bbcode) {
            var parts, part,
                lastIndex = 0;

            while ((parts = this._.bbcPartsRegex.exec(bbcode))) {
                var tagIndex = parts.index;
                if (tagIndex > lastIndex) {
                    var text = bbcode.substring(lastIndex, tagIndex);
                    this.onText(text, 1);
                }

                lastIndex = this._.bbcPartsRegex.lastIndex;

                // "parts" is an array with the following items:
                // 0 : The entire match for opening/closing tags and line-break;
                // 1 : line-break;
                // 2 : open of tag excludes option;
                // 3 : tag option;
                // 4 : close of tag;

                part = (parts[1] || parts[3] || '').toLowerCase();
                // Unrecognized tags should be delivered as a simple text (#7860).
                if (part && !bbcodeMap[part]) {
                    this.onText(parts[0]);
                    continue;
                }

                // Opening tag
                if (parts[1]) {
                    var tagName = bbcodeMap[part],
                        attribs = {},
                        styles = {},
                        optionPart = parts[2];

                    if (optionPart) {
                        if (part == 'list') {
                            if (!isNaN(optionPart))
                                optionPart = 'decimal';
                            else if (/^[a-z]+$/.test(optionPart))
                                optionPart = 'lower-alpha';
                            else if (/^[A-Z]+$/.test(optionPart))
                                optionPart = 'upper-alpha';
                        }

                        if (stylesMap[part]) {
                            // Font size represents percentage.
                            if (part == 'size')
                                optionPart += '%';

                            styles[stylesMap[part]] = optionPart;
                            attribs.style = serializeStyleText(styles);
                        } else if (attributesMap[part])
                            attribs[attributesMap[part]] = optionPart;
                    }

                    // Two special handling - image and email, protect them
                    // as "span" with an attribute marker.
                    if (part == 'email' || part == 'img')
                        attribs['bbcode'] = part;

                    this.onTagOpen(tagName, attribs, CKEDITOR.dtd.$empty[tagName]);
                }
                // Closing tag
                else if (parts[3])
                    this.onTagClose(bbcodeMap[part]);
            }

            if (bbcode.length > lastIndex)
                this.onText(bbcode.substring(lastIndex, bbcode.length), 1);
        }
    };

    /**
     * Creates a {@link CKEDITOR.htmlParser.fragment} from an HTML string.
     *
     *        var fragment = CKEDITOR.htmlParser.fragment.fromHtml( '<b>Sample</b> Text' );
     *        alert( fragment.children[ 0 ].name );        // 'b'
     *        alert( fragment.children[ 1 ].value );    // ' Text'
     *
     * @static
     * @member CKEDITOR.htmlParser.fragment
     * @param {String} source The HTML to be parsed, filling the fragment.
     * @returns {CKEDITOR.htmlParser.fragment} The fragment created.
     */
    CKEDITOR.htmlParser.fragment.fromBBCode = function (source) {
        var parser = new CKEDITOR.BBCodeParser(),
            fragment = new CKEDITOR.htmlParser.fragment(),
            pendingInline = [],
            pendingBrs = 0,
            currentNode = fragment,
            returnPoint;

        function checkPending(newTagName) {
            if (pendingInline.length > 0) {
                for (var i = 0; i < pendingInline.length; i++) {
                    var pendingElement = pendingInline[i],
                        pendingName = pendingElement.name,
                        pendingDtd = CKEDITOR.dtd[pendingName],
                        currentDtd = currentNode.name && CKEDITOR.dtd[currentNode.name];

                    if ((!currentDtd || currentDtd[pendingName]) && (!newTagName || !pendingDtd || pendingDtd[newTagName] || !CKEDITOR.dtd[newTagName])) {
                        // Get a clone for the pending element.
                        pendingElement = pendingElement.clone();

                        // Add it to the current node and make it the current,
                        // so the new element will be added inside of it.
                        pendingElement.parent = currentNode;
                        currentNode = pendingElement;

                        // Remove the pending element (back the index by one
                        // to properly process the next entry).
                        pendingInline.splice(i, 1);
                        i--;
                    }
                }
            }
        }

        function checkPendingBrs(tagName, closing) {
            var len = currentNode.children.length,
                previous = len > 0 && currentNode.children[len - 1],
                lineBreakParent = !previous && writer.getRule(tagnameMap[currentNode.name], 'breakAfterOpen'),
                lineBreakPrevious = previous && previous.type == CKEDITOR.NODE_ELEMENT && writer.getRule(tagnameMap[previous.name], 'breakAfterClose'),
                lineBreakCurrent = tagName && writer.getRule(tagnameMap[tagName], closing ? 'breakBeforeClose' : 'breakBeforeOpen');

            if (pendingBrs && (lineBreakParent || lineBreakPrevious || lineBreakCurrent))
                pendingBrs--;

            // 1. Either we're at the end of block, where it requires us to compensate the br filler
            // removing logic (from htmldataprocessor).
            // 2. Or we're at the end of pseudo block, where it requires us to compensate
            // the bogus br effect.
            if (pendingBrs && tagName in blockLikeTags)
                pendingBrs++;

            while (pendingBrs && pendingBrs--)
                currentNode.children.push(previous = new CKEDITOR.htmlParser.element('br'));
        }

        function addElement(node, target) {
            checkPendingBrs(node.name, 1);

            target = target || currentNode || fragment;

            var len = target.children.length,
                previous = len > 0 && target.children[len - 1] || null;

            node.previous = previous;
            node.parent = target;

            target.children.push(node);

            if (node.returnPoint) {
                currentNode = node.returnPoint;
                delete node.returnPoint;
            }
        }

        parser.onTagOpen = function (tagName, attributes, selfClosing) {
            var element = new CKEDITOR.htmlParser.element(tagName, attributes);

            // This is a tag to be removed if empty, so do not add it immediately.
            if (CKEDITOR.dtd.$removeEmpty[tagName]) {
                pendingInline.push(element);
                return;
            }

            var currentName = currentNode.name;

            var currentDtd = currentName && (CKEDITOR.dtd[currentName] || (currentNode._.isBlockLike ? CKEDITOR.dtd.div : CKEDITOR.dtd.span));

            // If the element cannot be child of the current element.
            if (currentDtd && !currentDtd[tagName]) {
                var reApply = false,
                    addPoint; // New position to start adding nodes.

                // If the element name is the same as the current element name,
                // then just close the current one and append the new one to the
                // parent. This situation usually happens with <p>, <li>, <dt> and
                // <dd>, specially in IE. Do not enter in this if block in this case.
                if (tagName == currentName)
                    addElement(currentNode, currentNode.parent);
                else if (tagName in CKEDITOR.dtd.$listItem) {
                    parser.onTagOpen('ul', {});
                    addPoint = currentNode;
                    reApply = true;
                } else {
                    addElement(currentNode, currentNode.parent);

                    // The current element is an inline element, which
                    // cannot hold the new one. Put it in the pending list,
                    // and try adding the new one after it.
                    pendingInline.unshift(currentNode);
                    reApply = true;
                }

                if (addPoint)
                    currentNode = addPoint;
                // Try adding it to the return point, or the parent element.
                else
                    currentNode = currentNode.returnPoint || currentNode.parent;

                if (reApply) {
                    parser.onTagOpen.apply(this, arguments);
                    return;
                }
            }

            checkPending(tagName);
            checkPendingBrs(tagName);

            element.parent = currentNode;
            element.returnPoint = returnPoint;
            returnPoint = 0;

            if (element.isEmpty)
                addElement(element);
            else
                currentNode = element;
        };

        parser.onTagClose = function (tagName) {
            // Check if there is any pending tag to be closed.
            for (var i = pendingInline.length - 1; i >= 0; i--) {
                // If found, just remove it from the list.
                if (tagName == pendingInline[i].name) {
                    pendingInline.splice(i, 1);
                    return;
                }
            }

            var pendingAdd = [],
                newPendingInline = [],
                candidate = currentNode;

            while (candidate.type && candidate.name != tagName) {
                // If this is an inline element, add it to the pending list, if we're
                // really closing one of the parents element later, they will continue
                // after it.
                if (!candidate._.isBlockLike)
                    newPendingInline.unshift(candidate);

                // This node should be added to it's parent at this point. But,
                // it should happen only if the closing tag is really closing
                // one of the nodes. So, for now, we just cache it.
                pendingAdd.push(candidate);

                candidate = candidate.parent;
            }

            if (candidate.type) {
                // Add all elements that have been found in the above loop.
                for (i = 0; i < pendingAdd.length; i++) {
                    var node = pendingAdd[i];
                    addElement(node, node.parent);
                }

                currentNode = candidate;


                addElement(candidate, candidate.parent);

                // The parent should start receiving new nodes now, except if
                // addElement changed the currentNode.
                if (candidate == currentNode)
                    currentNode = currentNode.parent;

                pendingInline = pendingInline.concat(newPendingInline);
            }
        };

        parser.onText = function (text) {
            var currentDtd = CKEDITOR.dtd[currentNode.name];
            if (!currentDtd || currentDtd['#']) {
                checkPendingBrs();
                checkPending();

                text.replace(/(\r\n|[\r\n])|[^\r\n]*/g, function (piece, lineBreak) {
                    if (lineBreak !== undefined && lineBreak.length)
                        pendingBrs++;
                    else if (piece.length) {
                        var lastIndex = 0;

                        // Create smiley from text emotion.
                        piece.replace(smileyRegExp, function (match, index) {
                            addElement(new CKEDITOR.htmlParser.text(piece.substring(lastIndex, index)), currentNode);
                            addElement(new CKEDITOR.htmlParser.element('smiley', {desc: smileyReverseMap[match]}), currentNode);
                            lastIndex = index + match.length;
                        });

                        if (lastIndex != piece.length)
                            addElement(new CKEDITOR.htmlParser.text(piece.substring(lastIndex, piece.length)), currentNode);
                    }
                });
            }
        };

        // Parse it.
        parser.parse(CKEDITOR.tools.htmlEncode(source));

        // Close all hanging nodes.
        while (currentNode.type != CKEDITOR.NODE_DOCUMENT_FRAGMENT) {
            var parent = currentNode.parent,
                node = currentNode;

            addElement(node, parent);
            currentNode = parent;
        }

        return fragment;
    };

    var BBCodeWriter = CKEDITOR.tools.createClass({
        $: function () {
            this._ = {
                output: [],
                rules: []
            };

            // List and list item.
            this.setRules('list', {breakBeforeOpen: 1, breakAfterOpen: 1, breakBeforeClose: 1, breakAfterClose: 1});

            this.setRules('*', {
                breakBeforeOpen: 1,
                breakAfterOpen: 0,
                breakBeforeClose: 1,
                breakAfterClose: 0
            });

            this.setRules('quote', {
                breakBeforeOpen: 1,
                breakAfterOpen: 0,
                breakBeforeClose: 0,
                breakAfterClose: 1
            });
        },

        proto: {
            //
            // Sets formatting rules for a given tag. The possible rules are:
            // <ul>
            //	<li><b>breakBeforeOpen</b>: break line before the opener tag for this element.</li>
            //	<li><b>breakAfterOpen</b>: break line after the opener tag for this element.</li>
            //	<li><b>breakBeforeClose</b>: break line before the closer tag for this element.</li>
            //	<li><b>breakAfterClose</b>: break line after the closer tag for this element.</li>
            // </ul>
            //
            // All rules default to "false". Each call to the function overrides
            // already present rules, leaving the undefined untouched.
            //
            // @param {String} tagName The tag name to which set the rules.
            // @param {Object} rules An object containing the element rules.
            // @example
            // // Break line before and after "img" tags.
            // writer.setRules( 'list',
            //		 {
            //				 breakBeforeOpen : true
            //				 breakAfterOpen : true
            //		 });
            setRules: function (tagName, rules) {
                var currentRules = this._.rules[tagName];

                if (currentRules)
                    CKEDITOR.tools.extend(currentRules, rules, true);
                else
                    this._.rules[tagName] = rules;
            },

            getRule: function (tagName, ruleName) {
                return this._.rules[tagName] && this._.rules[tagName][ruleName];
            },

            openTag: function (tag) {
                if (tag in bbcodeMap) {
                    if (this.getRule(tag, 'breakBeforeOpen'))
                        this.lineBreak(1);

                    this.write('[', tag);
                }
            },

            openTagClose: function (tag) {
                if (tag == 'br')
                    this._.output.push('\n');
                else if (tag in bbcodeMap) {
                    this.write(']');
                    if (this.getRule(tag, 'breakAfterOpen'))
                        this.lineBreak(1);
                }
            },

            attribute: function (name, val) {
                if (name == 'option') {
                    // Force simply ampersand in attributes.
                    if (typeof val == 'string')
                        val = val.replace(/&amp;/g, '&');

                    this.write('=', val);
                }
            },

            closeTag: function (tag) {
                if (tag in bbcodeMap) {
                    if (this.getRule(tag, 'breakBeforeClose'))
                        this.lineBreak(1);

                    tag != '*' && this.write('[/', tag, ']');

                    if (this.getRule(tag, 'breakAfterClose'))
                        this.lineBreak(1);
                }
            },

            text: function (text) {
                this.write(text);
            },

            comment: function () {
            },

            // Output line-break for formatting.
            lineBreak: function () {
                // Avoid line break when:
                // 1) Previous tag already put one.
                // 2) We're at output start.
                if (!this._.hasLineBreak && this._.output.length) {
                    this.write('\n');
                    this._.hasLineBreak = 1;
                }
            },

            write: function () {
                this._.hasLineBreak = 0;
                var data = Array.prototype.join.call(arguments, '');
                this._.output.push(data);
            },

            reset: function () {
                this._.output = [];
                this._.hasLineBreak = 0;
            },

            getHtml: function (reset) {
                var bbcode = this._.output.join('');

                if (reset)
                    this.reset();

                return decodeHtml(bbcode);
            }
        }
    });

    var writer = new BBCodeWriter();

    CKEDITOR.plugins.add('bbcode', {
        requires: 'entities',

        // Adapt some critical editor configuration for better support
        // of BBCode environment.
        beforeInit: function (editor) {
            var config = editor.config;

            CKEDITOR.tools.extend(config, {
                // This one is for backwards compatibility only as
                // editor#enterMode is already set at this stage (#11202).
                enterMode: CKEDITOR.ENTER_BR,
                basicEntities: false,
                entities: false,
                fillEmptyBlocks: false
            }, true);

            editor.filter.disable();

            // Since CKEditor 4.3, editor#(active)enterMode is set before
            // beforeInit. Properties got to be updated (#11202).
            editor.activeEnterMode = editor.enterMode = CKEDITOR.ENTER_BR;
        },

        init: function (editor) {
            var config = editor.config;

            function BBCodeToHtml(code) {
                var fragment = CKEDITOR.htmlParser.fragment.fromBBCode(code),
                    writer = new CKEDITOR.htmlParser.basicWriter();

                fragment.writeHtml(writer, bbcodeFilter);
                return writer.getHtml(true);
            }

            var bbcodeFilter = new CKEDITOR.htmlParser.filter();
            bbcodeFilter.addRules({
                elements: {
                    blockquote: function (element) {
                        var quoted = new CKEDITOR.htmlParser.element('div');
                        quoted.children = element.children;
                        element.children = [quoted];
                        var citeText = element.attributes.cite;
                        if (citeText) {
                            var cite = new CKEDITOR.htmlParser.element('cite');
                            cite.add(new CKEDITOR.htmlParser.text(citeText.replace(/^"|"$/g, '')));
                            delete element.attributes.cite;
                            element.children.unshift(cite);
                        }
                    },
                    span: function (element) {
                        var bbcode;
                        if ((bbcode = element.attributes.bbcode)) {
                            if (bbcode == 'img') {
                                element.name = 'img';
                                element.attributes.src = element.children[0].value;
                                element.children = [];
                            } else if (bbcode == 'email') {
                                element.name = 'a';
                                element.attributes.href = 'mailto:' + element.children[0].value;
                            }

                            delete element.attributes.bbcode;
                        }
                    },
                    ol: function (element) {
                        if (element.attributes.listType) {
                            if (element.attributes.listType != 'decimal')
                                element.attributes.style = 'list-style-type:' + element.attributes.listType;
                        } else
                            element.name = 'ul';

                        delete element.attributes.listType;
                    },
                    a: function (element) {
                        if (!element.attributes.href)
                            element.attributes.href = element.children[0].value;
                    },
                    smiley: function (element) {
                        element.name = 'img';

                        var description = element.attributes.desc,
                            image = config.smiley_images[CKEDITOR.tools.indexOf(config.smiley_descriptions, description)],
                            src = CKEDITOR.tools.htmlEncode(config.smiley_path + image);

                        element.attributes = {
                            src: src,
                            'data-cke-saved-src': src,
                            title: description,
                            alt: description
                        };
                    }
                }
            });

            editor.dataProcessor.htmlFilter.addRules({
                elements: {
                    $: function (element) {
                        var attributes = element.attributes,
                            style = CKEDITOR.tools.parseCssText(attributes.style, 1),
                            value;

                        var tagName = element.name;
                        if (tagName in convertMap)
                            tagName = convertMap[tagName];
                        else if (tagName == 'span') {
                            if ((value = style.color)) {
                                tagName = 'color';
                                value = CKEDITOR.tools.convertRgbToHex(value);
                            } else if ((value = style['font-size'])) {
                                var percentValue = value.match(/(\d+)%$/);
                                if (percentValue) {
                                    value = percentValue[1];
                                    tagName = 'size';
                                }
                            }
                        } else if (tagName == 'ol' || tagName == 'ul') {
                            if ((value = style['list-style-type'])) {
                                switch (value) {
                                    case 'lower-alpha':
                                        value = 'a';
                                        break;
                                    case 'upper-alpha':
                                        value = 'A';
                                        break;
                                }
                            } else if (tagName == 'ol')
                                value = 1;

                            tagName = 'list';
                        } else if (tagName == 'blockquote') {
                            try {
                                var cite = element.children[0],
                                    quoted = element.children[1],
                                    citeText = cite.name == 'cite' && cite.children[0].value;

                                if (citeText) {
                                    value = '"' + citeText + '"';
                                    element.children = quoted.children;
                                }

                            } catch (er) {
                            }

                            tagName = 'quote';
                        } else if (tagName == 'a') {
                            if ((value = attributes.href)) {
                                if (value.indexOf('mailto:') !== -1) {
                                    tagName = 'email';
                                    // [email] should have a single text child with email address.
                                    element.children = [new CKEDITOR.htmlParser.text(value.replace('mailto:', ''))];
                                    value = '';
                                } else {
                                    var singleton = element.children.length == 1 && element.children[0];
                                    if (singleton && singleton.type == CKEDITOR.NODE_TEXT && singleton.value == value)
                                        value = '';

                                    tagName = 'url';
                                }
                            }
                        } else if (tagName == 'img') {
                            element.isEmpty = 0;

                            // Translate smiley (image) to text emotion.
                            var src = attributes['data-cke-saved-src'] || attributes.src,
                                alt = attributes.alt;

                            if (src && src.indexOf(editor.config.smiley_path) != -1 && alt)
                                return new CKEDITOR.htmlParser.text(smileyMap[alt]);
                            else
                                element.children = [new CKEDITOR.htmlParser.text(src)];
                        }

                        element.name = tagName;
                        value && (element.attributes.option = value);

                        return null;
                    },

                    // Remove any bogus br from the end of a pseudo block,
                    // e.g. <div>some text<br /><p>paragraph</p></div>
                    br: function (element) {
                        var next = element.next;
                        if (next && next.name in blockLikeTags)
                            return false;
                    }
                }
            }, 1);

            editor.dataProcessor.writer = writer;

            function onSetData(evt) {
                var bbcode = evt.data.dataValue;
                evt.data.dataValue = BBCodeToHtml(bbcode);
            }

            // Skip the first "setData" call from inline creator, to allow content of
            // HTML to be loaded from the page element.
            if (editor.elementMode == CKEDITOR.ELEMENT_MODE_INLINE)
                editor.once('contentDom', function () {
                    editor.on('setData', onSetData);
                });
            else
                editor.on('setData', onSetData);

        },

        afterInit: function (editor) {
            var filters;
            if (editor._.elementsPath) {
                // Eliminate irrelevant elements from displaying, e.g body and p.
                if ((filters = editor._.elementsPath.filters)) {
                    filters.push(function (element) {
                        var htmlName = element.getName(),
                            name = tagnameMap[htmlName] || false;

                        // Specialized anchor presents as email.
                        if (name == 'link' && element.getAttribute('href').indexOf('mailto:') === 0)
                            name = 'email';
                        // Styled span could be either size or color.
                        else if (htmlName == 'span') {
                            if (element.getStyle('font-size'))
                                name = 'size';
                            else if (element.getStyle('color'))
                                name = 'color';
                        } else if (name == 'img') {
                            var src = element.data('cke-saved-src') || element.getAttribute('src');
                            if (src && src.indexOf(editor.config.smiley_path) === 0)
                                name = 'smiley';
                        }

                        return name;
                    });
                }
            }
        }
    });

})();
